package com.chen.runnabletask;

import com.chen.entry.User;
import com.chen.service.ServiceTC;
import com.chen.util.Constant;
import com.chen.util.MssageStringUtils;

import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class ServiceThread implements Runnable {
    private DataInputStream in;
    private DataOutputStream out;
    // 因为将用户名称最为集合的键，所以需要在这里额外定义成员变量
    private String userName;
    private final Socket socket;

    public ServiceThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            in = new DataInputStream(socket.getInputStream());
            out = new DataOutputStream(socket.getOutputStream());
            // 使用循环，多次读取单个用户的输入
            while (true) {
                String type = in.readUTF();
                switch (type) {
                    case Constant.LOGIN_TYPE:
                        // 登录信息,如果登录成功，则会更新用户列表
                        login();
                        break;
                    case Constant.ALL_MSG:
                        // 群聊信息
                        sendMsgToAll();
                        break;
                    case Constant.PERSONAL_MSG:
                        // 私聊信息
                        sendMsgToPersonal();
                        break;
                }
            }
        } catch (Exception e) {
            System.out.println("有客户端下线了，IP地址: " + socket.getInetAddress().getHostAddress() + "端口号：" + socket.getPort());
            // 客户端下线，将用户溢出在线列表
            ServiceTC.userSocketMap.remove(userName);
            // 再次更新用户列表
            updateOnLineUserList();
        }
    }

    // 服务端将私聊的消息发送给自己和对方
    private void sendMsgToPersonal() {
        try {
            // 需要获取私聊对象的，这里采取根据用户名获取对应的通信管道
            String msgToUser = in.readUTF();
            // 封装要发送的消息字符串
            String msgStr = MssageStringUtils.getMsgStr(in.readUTF(), userName);
            // 根据用户名获取对应的通信管道
            Socket socketToUser = ServiceTC.userSocketMap.get(msgToUser);
            // 给私聊对象发送消息，首先要获取管道的输出流
            DataOutputStream dos = new DataOutputStream(socketToUser.getOutputStream());
            // 先发送事前约定好的的消息类型
            dos.writeUTF(Constant.PERSONAL_MSG);
            // 然后再发送消息本身
            dos.writeUTF(msgStr);
            // 将接收人也一起发出去
            dos.writeUTF(msgToUser);
            // 将发送人也一起发出去
            dos.writeUTF(userName);
            // 最后，刷新管道，将数据写出去
            dos.flush();
        } catch (IOException e) {
            System.out.println("服务端：接收私聊消息是发送错误");
            e.printStackTrace();
        }
    }

    /**
     * 收到的客户端消息实现群发
     */
    private void sendMsgToAll() {
        try {
            // 这是将收到的客户端消息实现群发
            // 获取客户端收到的消息
            String msg = in.readUTF();
            String msgStr = MssageStringUtils.getMsgStr(msg, userName);
            // 获取所有在线的客户端，然后将消息发送给每个客户端
            for (Socket value : ServiceTC.userSocketMap.values()) {
                try {
                    // 使用try-with会自动释放资源，会导致通信管道被关闭,导致tcp连接被中断
                    // 因此要尽量避免
                    DataOutputStream dos = new DataOutputStream(value.getOutputStream());
                    // 首先发送给客户端，这次发送的消息类型
                    dos.writeUTF(Constant.ALL_MSG);
                    // 将消息内容发送给每个客户端
                    dos.writeUTF(msgStr);
                    // 刷新管道
                    dos.flush();
                } catch (Exception e) {
                    System.out.println("给客户端群发消息失败，请检查");
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            System.out.println("服务端sendMsgToAll()出错");
            e.printStackTrace();
        }
    }

    /**
     * 登录信息,如果登录成功，则会更新用户列表
     */
    private void login() {
        try {
            String userName = in.readUTF();
            this.userName = userName;
            String password = in.readUTF();
            List<User> userList = getUserListFromFile();
            // 1.需要先判断用户名是否会和已经注册的用户重复
            // 假设已经注册过的的数据，会保存到文件里
            boolean isUserNameDuplication = userList.stream()
                    .anyMatch(s -> s.getName().equals(userName) && !s.getPassword().equals(password));
            // 首先要判断当前用户是否已经注册过
            boolean isRegister = userList.stream()
                    .anyMatch(s -> s.getName().equals(userName) && s.getPassword().equals(password));
            // 判断是否登录成功，注册过，就已经认为登录了
            boolean result = isRegister;
            // 如果用户名不重复，执行注册和登录操作
            if (!isUserNameDuplication) {
                // 如果已经注册过，那就执行登录，否则就执行注册
                if (!isRegister) {
                    register(userName, password);
                    result = true;
                }
            }
            // 如果用户名重复，则提示用户重新输入用户名
            out.writeBoolean(isUserNameDuplication);
            // 将登录成功的结果返回给客户端
            out.writeBoolean(result);
            // 刷新管道
            out.flush();

            // 只有登录成才维护集合
            if (!isUserNameDuplication || isRegister) {
                // 登录成功之后，需要将维护集合userSocketMap
                ServiceTC.userSocketMap.put(userName, socket);

                // 同步更新用户列表
                updateOnLineUserList();
            }
        } catch (IOException e) {
            System.out.println("服务端login()出错");
            e.printStackTrace();
        }
    }

    /**
     * 步更新用户列表
     */
    private void updateOnLineUserList() {
        // 这里需要对每个客户端做同样的操作，所以需要获取到每个客户端的通信观岙
        // 先准备号用户名称的集合
        // 因为在登录的时候已经确保不会有重复的用户名
        Set<String> users = ServiceTC.userSocketMap.keySet();
        // 循环每个客户端
        for (Socket s : ServiceTC.userSocketMap.values()) {
            try {
                // 之前把这个资源放进try()with中，导致通信管道关闭，客户端接收出现错误，
                // 这个问题需要牢记
                DataOutputStream dos = new DataOutputStream(s.getOutputStream());
                // 先写入这次发的数据类型时UPDATE_USER
                dos.writeUTF(Constant.UPDATE_USER);
                // 2.将用户数传给客户端
                dos.writeInt(users.size());
                // 3.将用户循环写入客户端
                for (String user : users) {
                    dos.writeUTF(user);
                }
                // 4.刷新管道
                dos.flush();
            } catch (Exception e) {
                System.out.println("更新用户列表时出现错误，请检查！");
                e.printStackTrace();
            }
        }

    }

    /**
     * 注册，将用户名和密码用等号连接写入到文件
     *
     * @param userName 用户名称
     * @param password 密码
     */
    private void register(String userName, String password) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("net-tcp-study\\src\\com\\chen\\doc\\userInfo.txt", true))) {
            writer.write(userName);
            writer.write("=");
            writer.write(password);
            writer.newLine();
            writer.flush();
        } catch (Exception e) {
            System.out.println("文件写入失败，请检查！");
            e.printStackTrace();
        }
    }

    /**
     * 从文件读取用户信息
     *
     * @return 用户信息列表
     */
    private List<User> getUserListFromFile() {
        List<User> userList = new ArrayList<>();
        try (
                BufferedReader reader = new BufferedReader(new FileReader("net-tcp-study\\src\\com\\chen\\doc\\userInfo.txt"))
        ) {
            String userLine = reader.readLine();
            while (userLine != null) {
                // 这里约定，存入文件时以=号最为连接符
                String[] userInfo = userLine.split("=");
                userList.add(new User(userInfo[0], userInfo[1]));
                userLine = reader.readLine();
            }
        } catch (Exception e) {
            System.out.println("文件读取失败！请检查文件是否存在！");
            e.printStackTrace();
        }
        return userList;
    }
}
